package com.simpou.commons.utils.file;

import com.simpou.commons.utils.file.filter.DirectoryFileNameFilter;

import java.io.*;
import java.net.URL;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;

/**
 * Operações sobre arquivos.
 *
 * @author Jonas Pereira
 * @version 2012-07-16
 * @since 2011-06-23
 */
public final class FileHelper {

    /**
     * Filtro para listagem de diretórios somente.
     */
    private static final DirectoryFileNameFilter DIR_FN_FILTER = new DirectoryFileNameFilter();

    /**
     * Tamanha do buffer de leitura/escrita.
     */
    private static final int TAM_BUFFER = 2048;

    /**
     * ISO-8859-1.
     */
    public static final String ISO_ENCODING = "ISO-8859-1";

    /**
     * UTF-8.
     */
    public static final String UTF8_ENCODING = "UTF-8";

    /**
     * ISO-8859-1.
     */
    private static final String DEFAULT_ENCODING = ISO_ENCODING;

    /**
     * Lê um arquivo texto utilizando codificação padrão ISO-8859-1.
     *
     * @param filePath Caminho completo do arquivo.
     * @return Todo conteúdo do arquivo.
     * @throws java.io.IOException Erro ao acessar arquivo.
     */
    public static String read(final String filePath) throws IOException {
        return read(filePath, DEFAULT_ENCODING);
    }

    public static byte[] read(final File file) throws IOException {
        final RandomAccessFile f = new RandomAccessFile(file, "r");
        try {
            // Get and check length
            final long longlength = f.length();
            final int length = (int) longlength;
            if (length != longlength) {
                throw new IOException("File size >= 2 GB");
            }
            // Read file and return data
            final byte[] data = new byte[length];
            f.readFully(data);
            return data;
        } finally {
            f.close();
        }
    }

    /**
     * Lê um arquivo texto.
     *
     * @param filePath Caminho completo do arquivo.
     * @param encoding Codificação do arquivo.
     * @return Todo conteúdo do arquivo.
     * @throws java.io.IOException Erro ao acessar arquivo.
     */
    public static String read(final String filePath, final String encoding)
            throws IOException {
        File file = new File(filePath);

        checkRead(file);

        Scanner reader = new Scanner(file, encoding);
        StringBuilder bufSaida = new StringBuilder();
        String text = "";

        try {
            if (reader.hasNextLine()) {
                text = readLine(bufSaida, reader);
            }
        } finally {
            reader.close();
        }

        return text;
    }

    /**
     * Escreve em arquivo do tipo texto utilizando codificação padrão
     * ISO-8859-1.
     *
     * @param filePath Caminho completo do arquivo a ser criado ou editado.
     * @param text     Texto a ser escrito.
     * @param concat   Informa que o conteudo do arquivo existente nao deve ser
     *                 apagado e sim concatenado ao texto a ser escrito.
     * @param backup   Gera backup do arquivo alterado. Arquivo é gerado no mesmo
     *                 diretório com final baseado na data e hora no formato yyyyMMddHHmmss.
     * @return Nome do arquivo de backup.
     * @throws java.io.IOException      Erro ao acessar arquivo.
     * @throws java.text.ParseException Caso não consiga converter data atual.
     */
    public static String write(final String filePath, final String text,
                               final boolean concat, final boolean backup)
            throws IOException, ParseException {
        return write(filePath, text, concat, backup, DEFAULT_ENCODING);
    }

    /**
     * Escreve em arquivo do tipo texto. Cria se não existir.
     *
     * @param filePath Caminho completo do arquivo a ser criado ou editado.
     * @param text     Texto a ser escrito.
     * @param concat   Informa que o conteudo do arquivo existente nao deve ser
     *                 apagado e sim concatenado ao texto a ser escrito.
     * @param backup   Gera backup do arquivo alterado. Arquivo é gerado no mesmo
     *                 diretório com final baseado na data e hora no formato yyyyMMddHHmmss.
     * @param encoding Codificação do arquivo.
     * @return Nome do arquivo de backup.
     * @throws java.io.IOException      Erro ao acessar arquivo.
     * @throws java.text.ParseException Caso não consiga converter data atual.
     */
    public static String write(final String filePath, final String text,
                               final boolean concat, final boolean backup, final String encoding)
            throws IOException, ParseException {
        String bkpFilePath = null;

        // cria diretórios caso não existam
        mkdirParent(filePath);

        checkWrite(new File(filePath));

        if (backup) {
            // realiza backup
            String textOld = read(filePath);
            final DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            bkpFilePath = filePath + "." + dateFormat.format(new Date());
            checkWrite(new File(bkpFilePath));
            write(bkpFilePath, textOld, false, false, encoding);
        }

        // fluxos de escrita
        FileOutputStream fo = new FileOutputStream(filePath, concat);
        OutputStreamWriter os = new OutputStreamWriter(fo, encoding);
        Writer bw = new BufferedWriter(os);

        try {
            // escreve texto
            bw.write(text);
        } finally {
            // fecha arquivo
            bw.flush();
            bw.close();
            os.close();
            fo.close();
        }

        return bkpFilePath;
    }

    /**
     * <p>exists.</p>
     *
     * @param filePath Caminho completo do arquivo.
     * @return Se o arquivos existe ou não.
     */
    public static boolean exists(final String filePath) {
        File file = new File(filePath);

        return file.exists();
    }

    /**
     * Copia um arquivo binário ou textual.
     *
     * @param srcFilePath Caminho completo do arquivo de origem.
     * @param tgtFilePath Caminho completo do arquivo de destino.
     * @throws java.io.IOException Erro ao escrever bytes.
     */
    public static void copy(final String srcFilePath, final String tgtFilePath)
            throws IOException {
        checkMove(new File(srcFilePath), new File(tgtFilePath));

        mkdirParent(tgtFilePath);

        byte[] buffer = new byte[TAM_BUFFER];
        int bytesIn = 0;

        OutputStream os;
        InputStream is;

        is = new FileInputStream(srcFilePath);

        try {
            os = new FileOutputStream(tgtFilePath);

            try {
                while ((bytesIn = is.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesIn);
                }
            } finally {
                os.close();
            }
        } finally {
            is.close();
        }
    }

    /**
     * Move um arquivo.
     *
     * @param srcFilePath Caminho completo do arquivo a ser movido.
     * @param tgtFilePath Caminho completo do local para onde sera copiado o
     *                    arquivo.
     * @throws java.io.IOException Erro ao acessar arquivos.
     */
    public static void move(final String srcFilePath, final String tgtFilePath)
            throws IOException {
        copy(srcFilePath, tgtFilePath);

        try {
            delete(srcFilePath);
        } catch (Throwable ex) {
            //rollback
            delete(tgtFilePath);
            throw new RuntimeException(ex);
        }
    }

    /**
     * <p>length.</p>
     *
     * @param filePath Caminho absoluto do arquivo.
     * @return Tamanho de um arquivo em bytes.
     */
    public static long length(final String filePath) {
        return new File(filePath).length();
    }

    /**
     * <p>lastModified.</p>
     *
     * @param filePath Caminho absoluto do arquivo.
     * @return Data de modificacao de um arquivo.
     */
    public static Date lastModified(final String filePath) {
        return new Date(new File(filePath).lastModified());
    }

    /**
     * Cria árvore de diretórios vazia.
     *
     * @param dirPath Arvore de diretórios.
     * @return Se diretórios foram criados.
     */
    public static boolean mkdir(final String dirPath) {
        return new File(dirPath).mkdirs();
    }

    /**
     * <p>count.</p>
     *
     * @param dirPath Diretório pai.
     * @return Número de arquivos e diretórios filhos
     */
    public static int count(final String dirPath) {
        return new File(dirPath).listFiles().length;
    }

    /**
     * <p>name.</p>
     *
     * @param filePath Caminho absoluto do arquivo.
     * @return Nome do arquivo.
     */
    public static String name(final String filePath) {
        return new File(filePath).getName();
    }

    /**
     * Renomeia um arquivo.
     *
     * @param srcFilePath Arquivo a ser renomeado.
     * @param tgtFilePath Novo nome.
     * @return Se foi renomeado com sucesso.
     */
    public static boolean rename(final String srcFilePath,
                                 final String tgtFilePath) {
        File srcFile = new File(srcFilePath);
        File tgtFile = new File(tgtFilePath);

        checkMove(srcFile, tgtFile);

        String srcParent = srcFile.getParent();
        String tgtParent = tgtFile.getParent();

        if (!srcParent.equals(tgtParent)) {
            throw new IllegalArgumentException(
                    "Directory parent must be same and not null.");
        }

        return srcFile.renameTo(tgtFile);
    }

    /**
     * Deleta um recurso: arquivo ou diretório.
     *
     * @param path Caminho absoluto do recurso.
     * @return true se o recurso foi deletado.
     * @throws java.io.IOException Recurso não encontrado, protegido ou falha na
     *                             remoção.
     */
    public static boolean delete(final String path) throws IOException {
        File file = new File(path);
        checkWrite(file);

        return delete(file);
    }

    /**
     * Lista o conteudo de um diretório.
     *
     * @param dirPath   Caminho completo do diretório.
     * @param recursive "true" para listar conteudo de subdiretorios.
     * @param filter    Filtro de nomes. Padrões a serem incluídos.
     * @return Nome dos arquivos e subdiretórios contidos no diretorio.
     */
    public static List<String> list(final String dirPath,
                                    final boolean recursive, final FilenameFilter filter) {
        File fileDir = new File(dirPath);

        checkRead(fileDir);

        if (!fileDir.isDirectory()) {
            throw new IllegalArgumentException("Not a directory: " + dirPath);
        }

        List<String> files = new ArrayList<String>();

        if (recursive) {
            list(files, fileDir, filter);
        } else {
            File[] listFiles = fileDir.listFiles(filter);

            for (File file : listFiles) {
                files.add(file.getAbsolutePath());
            }
        }

        return files;
    }

    /**
     * <p>getDelimPattern.</p>
     *
     * @param delim Delimitador de arquivos.
     * @return Pattern adequado ao delimitador.
     */
    public static String getDelimPattern(final String delim) {
        String delimPattern;

        if (delim.equals("/")) {
            delimPattern = delim;
        } else {
            delimPattern = "\\\\";
        }

        return delimPattern;
    }

    /**
     * <p>getDelimPattern.</p>
     *
     * @return A partir do delimitador de arquivos do sistema escolhe o pattern
     *         adequado. Útil nos casos de manipulação de string como replace.
     */
    public static String getDelimPattern() {
        String delim = File.separator;

        return getDelimPattern(delim);
    }

    /**
     * <p>getResourceFile.</p>
     *
     * @param relPath Caminho relativo ao diretório base de recursos do projeto.
     * @return Objeto correspondente ao arquivo/diretório de recurso do projeto.
     * @throws FileNotFoundException Arquivo não encontrado.
     */
    public static File getResourceFile(final String relPath)
            throws FileNotFoundException {
        final ClassLoader loader = Thread.currentThread().getContextClassLoader();
        final URL url = loader.getResource(relPath);

        if (url == null) {
            throw new FileNotFoundException("Path: " + relPath);
        }

        File file = new File(url.getFile());
        final String winSpaceChar = "%20";
        String path;

        try {
            path = file.getCanonicalPath();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }

        // correção necessária no windows para caminhos com espaço
        if (path.contains(winSpaceChar)) {
            path = path.replaceAll(winSpaceChar, " ");
            file = new File(path);
        }

        return file;
    }

    /**
     * Coloca delimitador de caminho de arquivo no final. Usado para caminhos de
     * diretórios.
     *
     * @param dirPath Caminho do diretório.
     * @return Caminho do diretório com delimitador padrão no final.
     */
    public static String putDirPathEndDelim(final String dirPath) {
        if (dirPath.endsWith("/") || dirPath.endsWith("\\")) {
            return dirPath;
        } else {
            return dirPath + File.separator;
        }
    }

    /**
     * Normaliza todos delimitadores para barra, não usa contra barras.
     *
     * @param filePath Caminho do arquivo.
     * @return Caminho do arquivo normalizado.
     */
    public static String normalizeFilePathDelim(final String filePath) {
        String newfilePath;

        if (filePath.contains("\\")) {
            newfilePath = filePath.replaceAll("\\\\", "/");
        } else {
            newfilePath = filePath;
        }

        return newfilePath;
    }

    /**
     * Lista arquivos de um diretório recursivamente.
     *
     * @param files   Lista com os nomes abosolutos do arquivos.
     * @param fileDir Diretório de pesquisa.
     * @param filter  Filtro de nomes. Padrões a serem incluídos.
     */
    private static void list(final List<String> files, final File fileDir,
                             final FilenameFilter filter) {
        File[] listFiles = fileDir.listFiles(filter);
        File[] listDirs;

        for (File file : listFiles) {
            if (file.isDirectory()) {
                list(files, file, filter);
            }

            if (!files.contains(file.getAbsolutePath())) {
                files.add(file.getAbsolutePath());
            }
        }

        // o filtro pode remover da lista os diretórios de recursão
        if (filter != null) {
            listDirs = fileDir.listFiles(DIR_FN_FILTER);

            for (File file : listDirs) {
                list(files, file, filter);
            }
        }
    }

    /**
     * Delega a tarefa de apagar arquivo ou diretorio.
     *
     * @param file File.
     * @return true se o recurso foi deletado.
     */
    private static boolean delete(final File file) {
        return delete(file, true);
    }

    /**
     * Delega a tarefa de apagar arquivo ou diretorio.
     *
     * @param file     File.
     * @param noErrors Indica se houveram error anteriores na recursão.
     * @return true se o recurso foi deletado.
     */
    private static boolean delete(final File file, final boolean noErrors) {
        if (file.isDirectory()) {
            return deleteDir(file, noErrors);
        } else {
            return deleteFile(file);
        }
    }

    /**
     * Apaga um arquivo ou diretório.
     *
     * @param file File.
     * @return Se apagou corretamente ou não.
     */
    private static boolean deleteFile(final File file) {
        boolean deleted = file.delete();

        return deleted;
    }

    /**
     * Apaga um diretório recursivamente.
     *
     * @param file     File.
     * @param noErrors Define se ocorreram erros anteriores na recursão.
     * @return Diretório não apagado.
     */
    private static boolean deleteDir(final File file, final boolean noErrors) {
        if (!noErrors) {
            return noErrors;
        }

        File[] listFiles = file.listFiles();
        boolean deleteOK = true;

        for (File fileAux : listFiles) {
            deleteOK = delete(fileAux, deleteOK);
        }

        // se não conseguir apagar algum, não continua
        if (deleteOK) {
            deleteOK = deleteFile(file);
        }

        return deleteOK;
    }

    /**
     * @param builder StringBuilder inicializado.
     * @param scanner Scanner inicializado e com pelo menos uma linha.
     * @return Todas linhas de arquivo recursivamente.
     */
    private static String readLine(final StringBuilder builder,
                                   final Scanner scanner) {
        builder.append(scanner.nextLine());

        if (scanner.hasNextLine()) {
            builder.append("\n");
            readLine(builder, scanner);
        }

        return builder.toString();
    }

    /**
     * Cria diretórios pai caso não existam.
     *
     * @param filePath Caminho absoluto do arquivo.
     * @return Se diretórios foram criados.
     * @throws IOException IOException.
     */
    private static boolean mkdirParent(final String filePath)
            throws IOException {
        String fullFilePath = new File(filePath).getCanonicalPath();
        File parent = new File(fullFilePath).getParentFile();

        if (parent.exists()) {
            return true;
        } else {
            return parent.mkdirs();
        }
    }

    /**
     * Verifica se arquivo existe e pode ser lido.
     *
     * @param file Arquivo.
     */
    private static void checkRead(final File file) {
        if (!file.exists()) {
            throw new IllegalArgumentException("No such file: "
                    + file.getAbsolutePath());
        }

        if (!file.canRead()) {
            throw new IllegalArgumentException("Read protected: "
                    + file.getAbsolutePath());
        }
    }

    /**
     * Verifica se arquivo existe e pode ser escrito.
     *
     * @param file Arquivo.
     */
    private static void checkWrite(final File file) {
        String parent = file.getParent();
        File fileParent = new File(parent);

        if (!fileParent.canWrite()) {
            throw new IllegalArgumentException(
                    "Directory parent is write protected: "
                            + fileParent.getAbsolutePath());
        }

        if (file.exists() && !file.canWrite()) {
            throw new IllegalArgumentException("Write protected: "
                    + file.getAbsolutePath());
        }
    }

    /**
     * Verifica se arquivo existe e pode ser lido.
     *
     * @param srcFile Arquivo origem.
     * @param tgtFile Arquivo destino.
     */
    private static void checkMove(final File srcFile, final File tgtFile) {
        checkRead(srcFile);

        if (tgtFile.exists()) {
            throw new IllegalArgumentException("File exists: "
                    + tgtFile.getAbsolutePath());
        }
    }
}
